SPB Git

spb/autoscholar

Public
24.4 KB · 609 lines tsx
1import { bibtexFor, getPaperBySlug, plainCitationFor, risFor } from "@autoscholar/core";2import { parseBib } from "@autoscholar/qa";3import { publicArtifactKey } from "@autoscholar/storage";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import { CitationPanel } from "@/components/CitationPanel";8import { Disclosure } from "@/components/Disclosure";9import { IconCode, IconData, IconDoc, IconDownload } from "@/components/Icons";10import { MathText } from "@/components/MathText";11import { PaperCard } from "@/components/PaperCard";12import { PaperReader } from "@/components/PaperReader";13import { PublicationMetadata } from "@/components/PublicationMetadata";14import { ResearchProvenance } from "@/components/ResearchProvenance";15import { ScientificFigure } from "@/components/ScientificFigure";16import { SectionHeading } from "@/components/SectionHeading";17import { disciplineLabel, fmtBytes, fmtDate, shortHash } from "@/lib/format";18import { db, storage } from "@/lib/server/db";1920export const dynamic = "force-dynamic";2122async function load(slug: string) {23  try {24    return await getPaperBySlug(db(), slug);25  } catch {26    return null;27  }28}2930export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {31  const { slug } = await params;32  const p = await load(slug);33  if (!p) return { title: "Paper not found" };34  const base = process.env.APP_BASE_URL ?? "http://localhost:8470";35  return {36    title: p.version.title,37    description: p.version.abstract.slice(0, 300),38    alternates: { canonical: `/research/${p.paper.slug}` },39    robots: p.paper.synthetic ? { index: false, follow: false } : undefined,40    openGraph: {41      title: p.version.title,42      description: p.version.abstract.slice(0, 300),43      type: "article",44      url: `${base}/research/${p.paper.slug}`,45      publishedTime: p.version.publishedAt.toISOString(),46    },47    other: {48      citation_title: p.version.title,49      citation_publication_date: p.version.publishedAt.toISOString().slice(0, 10),50      citation_technical_report_number: p.paper.publicId,51      citation_technical_report_institution: "AutoScholar.ai",52      citation_pdf_url: `${base}${p.version.pdfPublicPath}`,53    },54  };55}5657type ArtifactEntry = {58  path: string;59  kind: string;60  sha256: string;61  bytes: number;62  contentType: string;63  publicPath: string;64};65function Dl({ a }: { a: ArtifactEntry }) {66  return (67    <li className="flex items-center justify-between gap-3 border-b py-2 text-sm last:border-0 hairline">68      <a69        href={`${a.publicPath}?download=1`}70        className="min-w-0 truncate font-mono text-xs no-underline hover:underline"71        title={a.path}72      >73        {a.path}74      </a>75      <span className="subtle flex-none text-[11px]">76        {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)}77      </span>78    </li>79  );80}8182export default async function PaperPage({ params }: { params: Promise<{ slug: string }> }) {83  const { slug } = await params;84  const p = await load(slug);85  if (!p) notFound();86  const base = process.env.APP_BASE_URL ?? "http://localhost:8470";87  const v = p.version;88  const meta = v.metadata as Record<string, unknown>;89  const withdrawn = meta.artifactsWithdrawn === true;90  const set = withdrawn ? [] : v.artifactSet;91  const url = `${base}/research/${p.paper.slug}`;92  const pub = new Date(v.publishedAt);93  const cite = {94    publicId: p.paper.publicId,95    title: v.title,96    year: pub.getUTCFullYear(),97    month: pub.getUTCMonth() + 1,98    versionNo: v.versionNo,99    url,100    keywords: v.keywords,101    abstract: v.abstract,102  };103  const figures = set.filter((a) => a.kind === "figure" && /\.(png|svg)$/i.test(a.path));104  const pdfFigures = set.filter((a) => a.kind === "figure" && /\.pdf$/i.test(a.path));105  const tex = set.filter((a) => a.kind === "manuscript" && /\.(tex|bib|cls|sty)$/i.test(a.path));106  const mainTex = set.find((a) => a.path === "manuscript/main.tex") ?? tex[0];107  const code = set.filter((a) => a.kind === "code");108  const data = set.filter((a) => a.kind === "data");109  const results = set.filter((a) => a.kind === "result" || a.kind === "table");110  const prov = (v.provenance ?? {}) as Record<string, unknown>;111  const qa = (v.qaSummary ?? {}) as {112    gates?: Array<{ gate: string; status: string; severity: string; message: string }>;113    warnings?: string[];114    policyVersion?: string;115    evaluatedAt?: string;116  };117  let bib: ReturnType<typeof parseBib> = [];118  const bibEntry = set.find((a) => a.path === "sources/references.bib");119  if (bibEntry) {120    try {121      bib = parseBib(122        (await storage().get(publicArtifactKey(p.paper.publicId, v.versionNo, bibEntry.path))).toString(123          "utf8",124        ),125      );126    } catch {127      bib = [];128    }129  }130  const jsonLd = {131    "@context": "https://schema.org",132    "@type": "ScholarlyArticle",133    headline: v.title,134    name: v.title,135    abstract: v.abstract,136    keywords: v.keywords.join(", "),137    datePublished: v.publishedAt.toISOString(),138    dateModified: p.paper.updatedAt.toISOString(),139    author: { "@type": "Organization", name: "AutoScholar (AI-generated research)", url: base },140    publisher: { "@type": "Organization", name: "AutoScholar.ai", url: base },141    identifier: p.paper.publicId,142    url,143    isAccessibleForFree: true,144    ...(set.length145      ? {146          encoding: {147            "@type": "MediaObject",148            contentUrl: `${base}${v.pdfPublicPath}`,149            encodingFormat: "application/pdf",150          },151        }152      : {}),153    about: disciplineLabel(p.paper.discipline),154    version: v.versionNo,155    ...(p.retraction ? { creativeWorkStatus: "Retracted" } : {}),156  };157  const verifiedLabel = qa.gates158    ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} gates`159    : undefined;160  const contributions = Array.isArray(meta.contributions) ? (meta.contributions as string[]) : [];161  const sections: Array<[string, string]> = [162    ["Methods", typeof meta.methods === "string" ? meta.methods : ""],163    ["Main results", typeof meta.mainResults === "string" ? meta.mainResults : ""],164    ["Limitations", typeof meta.limitations === "string" ? meta.limitations : ""],165  ].filter(([, v]) => v) as Array<[string, string]>;166  const downloads = withdrawn167    ? []168    : [169        { href: `${v.pdfPublicPath}?download=1`, label: "PDF", icon: <IconDoc size={16} />, primary: true },170        ...(mainTex171          ? [172              {173                href: `${mainTex.publicPath}?download=1`,174                label: "LaTeX source",175                icon: <span className="font-mono text-[11px]">TeX</span>,176              },177            ]178          : []),179        ...(code.length180          ? [{ href: "#artifacts", label: `Code (${code.length})`, icon: <IconCode size={16} /> }]181          : []),182        ...(data.length183          ? [{ href: "#artifacts", label: `Data (${data.length})`, icon: <IconData size={16} /> }]184          : []),185      ];186  return (187    <article className="pb-20 lg:pb-0">188      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD built from server data */}189      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />190191      {/* Publication header */}192      <div style={{ background: "var(--bg-muted)" }} className="border-b hairline">193        <div className="container-x py-8 sm:py-12">194          {p.retraction && (195            <div196              role="alert"197              className="mb-6 rounded-lg border p-4"198              style={{199                borderColor: "var(--color-danger-500)",200                background: "color-mix(in oklab, var(--color-danger-500) 8%, transparent)",201              }}202            >203              <p className="font-semibold">204                This working paper was retracted on {fmtDate(p.retraction.retractedAt)}.205              </p>206              <p className="mt-1 text-sm">{p.retraction.notice}</p>207              {withdrawn && (208                <p className="muted mt-1 text-sm">209                  Its artifacts have been withdrawn from delivery. This page remains as a permanent record.210                </p>211              )}212            </div>213          )}214          <div className="grid gap-8 lg:grid-cols-12">215            <div className="lg:col-span-8">216              <PublicationMetadata217                publicId={p.paper.publicId}218                versionNo={v.versionNo}219                status={p.paper.status}220                synthetic={p.paper.synthetic}221                title={v.title}222                discipline={p.paper.discipline}223                mode={p.paper.researchMode}224                firstPublishedAt={p.paper.firstPublishedAt}225                revisedAt={v.publishedAt}226                keywords={v.keywords}227                verified={verifiedLabel}228              />229            </div>230            <aside className="lg:col-span-4" aria-label="Downloads">231              <div className="surface p-4">232                <p className="label mb-3">Artifacts</p>233                {downloads.length === 0 ? (234                  <p className="muted text-sm">Artifacts withdrawn.</p>235                ) : (236                  <ul className="grid grid-cols-2 gap-2 lg:grid-cols-1 [&>li:only-child]:col-span-2">237                    {downloads.map((dl) => (238                      <li key={dl.label}>239                        <a240                          className={`btn w-full justify-between ${dl.primary ? "btn-primary" : ""}`}241                          href={dl.href}242                        >243                          <span className="inline-flex items-center gap-2">244                            {dl.icon}245                            {dl.label}246                          </span>247                          <IconDownload size={14} />248                        </a>249                      </li>250                    ))}251                  </ul>252                )}253                <div className="mt-4">254                  <Disclosure compact />255                </div>256              </div>257            </aside>258          </div>259        </div>260      </div>261262      <div className="container-x">263        {/* Abstract + findings */}264        <section aria-labelledby="abstract" className="section-tight grid gap-10 lg:grid-cols-12">265          <div className="lg:col-span-8">266            <h2 id="abstract" className="eyebrow">267              Abstract268            </h2>269            <MathText text={v.abstract} className="prose-editorial dropcap mt-3" />270            {contributions.length > 0 && (271              <div className="mt-10">272                <h2 className="eyebrow">Key findings</h2>273                <ol className="mt-3 space-y-3">274                  {contributions.map((c, i) => (275                    <li key={c} className="grid grid-cols-[2rem_1fr] gap-3">276                      <span className="serif mt-0.5 text-xl" style={{ color: "var(--accent)" }}>277                        {i + 1}278                      </span>279                      <MathText text={c} className="prose-paper" />280                    </li>281                  ))}282                </ol>283              </div>284            )}285            {sections.map(([h, body]) => (286              <div key={h} className="mt-10">287                <h2 className="eyebrow">{h}</h2>288                <MathText text={body} className="prose-paper mt-3" />289              </div>290            ))}291          </div>292          <div className="space-y-4 lg:col-span-4">293            <div className="surface-muted p-4 text-sm">294              <p className="label mb-2">Research status</p>295              <ul className="space-y-1.5">296                <li>AutoScholar working paper (preprint)</li>297                <li>Produced autonomously by an AI research workflow</li>298                <li>299                  Automated critical review; <strong>not</strong> human peer review300                </li>301                <li>302                  Deterministic quality gates:{" "}303                  {qa.gates304                    ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} passed`305                    : "see provenance"}306                </li>307                <li>DOI: none (not registered)</li>308              </ul>309            </div>310            {p.summary.topics.length > 0 && (311              <div className="surface-muted p-4 text-sm">312                <p className="label mb-2">Topics</p>313                <ul className="flex flex-wrap gap-1.5">314                  {p.summary.topics.map((x) => (315                    <li key={x.slug}>316                      <Link317                        href={`/topics/${x.slug}`}318                        className="chip no-underline"319                        style={{ minHeight: "1.75rem" }}320                      >321                        {x.name}322                      </Link>323                    </li>324                  ))}325                </ul>326              </div>327            )}328            <nav aria-label="On this page" className="surface-muted hidden p-4 text-sm lg:block">329              <p className="label mb-2">On this page</p>330              <ul className="space-y-1.5">331                {[332                  ["#abstract", "Abstract"],333                  !withdrawn && ["#pdf", "Read the paper"],334                  (figures.length || pdfFigures.length) && ["#figures", "Figures"],335                  bib.length && ["#refs", "References"],336                  (code.length || data.length || results.length) && ["#artifacts", "Code and data"],337                  ["#provenance", "Provenance"],338                  ["#cite", "Cite"],339                  ["#history", "Versions"],340                ]341                  .filter(Boolean)342                  .map((x) => {343                    const [href, label] = x as [string, string];344                    return (345                      <li key={href}>346                        <a href={href} className="muted no-underline hover:text-[var(--fg)]">347                          {label}348                        </a>349                      </li>350                    );351                  })}352              </ul>353            </nav>354          </div>355        </section>356357        {/* Reader */}358        {!withdrawn && (359          <section aria-labelledby="pdf" className="section-tight border-t hairline">360            <SectionHeading eyebrow="Manuscript" title="Read the paper" id="pdf">361              Rendered from the released PDF. Use the arrow keys or the controls to turn pages; full-screen is362              available.363            </SectionHeading>364            <div className="mt-6">365              <PaperReader366                src={v.pdfPublicPath}367                title={`${p.paper.publicId} v${v.versionNo} — ${v.title}`}368                downloadHref={`${v.pdfPublicPath}?download=1`}369                versionLabel={`v${v.versionNo}`}370              />371            </div>372          </section>373        )}374375        {/* Figures */}376        {(figures.length > 0 || pdfFigures.length > 0) && (377          <section aria-labelledby="figures" className="section-tight border-t hairline">378            <SectionHeading eyebrow="Evidence" title="Figures" id="figures" tone="sci">379              Figures are the actual files produced by the study, served from the immutable artifact store.380            </SectionHeading>381            <ul className="mt-6 grid gap-5 md:grid-cols-2">382              {figures.map((f, i) => {383                const vec = pdfFigures.find(384                  (x) => x.path.replace(/\.pdf$/i, "") === f.path.replace(/\.(png|svg)$/i, ""),385                );386                return (387                  <li key={f.path}>388                    <ScientificFigure389                      src={f.publicPath}390                      path={f.path}391                      bytes={f.bytes}392                      index={i + 1}393                      vector={vec ? { href: vec.publicPath, bytes: vec.bytes } : undefined}394                    />395                  </li>396                );397              })}398              {pdfFigures399                .filter(400                  (f) =>401                    !figures.some(402                      (x) => x.path.replace(/\.(png|svg)$/i, "") === f.path.replace(/\.pdf$/i, ""),403                    ),404                )405                .map((f) => (406                  <li key={f.path} className="surface p-4 text-sm">407                    <a href={`${f.publicPath}?download=1`} className="font-mono text-xs">408                      {f.path.replace("results/figures/", "")}409                    </a>{" "}410                    <span className="subtle">(vector PDF, {fmtBytes(f.bytes)})</span>411                  </li>412                ))}413            </ul>414          </section>415        )}416417        {/* References */}418        {bib.length > 0 && (419          <section aria-labelledby="refs" className="section-tight border-t hairline">420            <SectionHeading eyebrow="Bibliography" title={`References (${bib.length})`} id="refs">421              Every reference was verified against a scholarly registry before publication; identifiers link422              to the source of record.423            </SectionHeading>424            <ol className="mt-6 grid gap-x-10 gap-y-3 text-sm md:grid-cols-2">425              {bib.map((b, i) => {426                const f = b.fields;427                const href = f.doi428                  ? `https://doi.org/${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`429                  : (f.url ?? (f.eprint ? `https://arxiv.org/abs/${f.eprint}` : undefined));430                return (431                  <li432                    key={b.key}433                    id={`ref-${b.key}`}434                    className="grid grid-cols-[2rem_1fr] gap-2 border-b pb-3 hairline"435                  >436                    <span className="subtle font-mono text-xs">[{i + 1}]</span>437                    <span className="min-w-0">438                      <span>{f.author ? `${f.author.replace(/\s+and\s+/g, ", ")}. ` : ""}</span>439                      {f.year && <span>({f.year}). </span>}440                      <span className="italic">{f.title?.replace(/[{}]/g, "")}</span>441                      {f.journal && <span>. {f.journal}</span>}442                      {f.booktitle && <span>. In {f.booktitle}</span>}443                      {f.volume && <span>, {f.volume}</span>}444                      {f.pages && <span>, {f.pages}</span>}.{" "}445                      {href && (446                        <a href={href} rel="noopener noreferrer" className="break-all font-mono text-xs">447                          {f.doi448                            ? `doi:${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`449                            : f.eprint450                              ? `arXiv:${f.eprint}`451                              : "link"}452                        </a>453                      )}454                    </span>455                  </li>456                );457              })}458            </ol>459          </section>460        )}461462        {/* Artifacts */}463        {(code.length > 0 || data.length > 0 || results.length > 0) && (464          <section id="artifacts" aria-labelledby="artifacts-h" className="section-tight border-t hairline">465            <SectionHeading466              eyebrow="Reproducibility"467              title="Code, data and results"468              id="artifacts-h"469              tone="sci"470            >471              Only files that were part of the study are listed. Each download carries its SHA-256 digest.472            </SectionHeading>473            <div className="mt-6 grid gap-5 lg:grid-cols-3">474              {[475                ["Code", code, <IconCode key="c" size={16} />],476                ["Data", data, <IconData key="d" size={16} />],477                ["Results and tables", results, <IconDoc key="r" size={16} />],478              ]479                .filter(([, list]) => (list as ArtifactEntry[]).length > 0)480                .map(([label, list, icon]) => (481                  <div key={label as string} className="surface p-4">482                    <p className="label mb-1 flex items-center gap-2">483                      {icon as React.ReactNode} {label as string} ({(list as ArtifactEntry[]).length})484                    </p>485                    <ul className="max-h-80 overflow-auto">486                      {(list as ArtifactEntry[]).map((a) => (487                        <Dl key={a.path} a={a} />488                      ))}489                    </ul>490                  </div>491                ))}492            </div>493          </section>494        )}495496        {/* Provenance */}497        <section id="provenance" aria-labelledby="provenance-h" className="section-tight border-t hairline">498          <SectionHeading eyebrow="Transparent record" title="Research provenance" id="provenance-h">499            How this paper was made: models, instruction digest, pipeline stages, verification policy and500            every gate result.501          </SectionHeading>502          <div className="mt-6">503            <ResearchProvenance prov={prov} qa={qa} texReleased={tex.length > 0} meta={meta} />504          </div>505        </section>506507        {/* Cite */}508        <section aria-labelledby="cite" className="section-tight border-t hairline">509          <SectionHeading eyebrow="Citation" title="Cite this working paper" id="cite" />510          <div className="mt-6 max-w-3xl">511            <CitationPanel512              slug={p.paper.slug}513              bibtex={bibtexFor(cite)}514              ris={risFor(cite)}515              plain={plainCitationFor(cite)}516            />517          </div>518        </section>519520        {/* History */}521        <section522          aria-labelledby="history"523          className="section-tight grid gap-8 border-t hairline md:grid-cols-2"524        >525          <div>526            <h2 id="history" className="title-2">527              Version history528            </h2>529            <ol className="mt-4 space-y-2 text-sm">530              {p.versions.map((x) => (531                <li key={x.id} className="surface flex items-start justify-between gap-3 p-3">532                  <div>533                    <span className="font-medium">Version {x.versionNo}</span>{" "}534                    <span className="muted">· {fmtDate(x.publishedAt)}</span>535                    <p className="muted mt-1">{x.changelog}</p>536                  </div>537                  {!withdrawn && (538                    <a className="btn btn-sm" href={`${x.pdfPublicPath}?download=1`}>539                      PDF v{x.versionNo}540                    </a>541                  )}542                </li>543              ))}544            </ol>545          </div>546          <div>547            <h2 className="title-2">Corrections and notices</h2>548            {p.corrections.length === 0 && !p.retraction ? (549              <p className="muted mt-4 text-sm">No corrections have been issued.</p>550            ) : (551              <ul className="mt-4 space-y-2 text-sm">552                {p.corrections.map((c) => (553                  <li key={c.id} className="surface p-3">554                    <span className="font-medium">555                      Correction v{c.fromVersionNo} → v{c.toVersionNo}556                    </span>{" "}557                    <span className="muted">· {fmtDate(c.createdAt)}</span>558                    <p className="mt-1">{c.notice}</p>559                  </li>560                ))}561                {p.retraction && (562                  <li className="surface p-3">563                    <span className="font-medium">Retraction</span>{" "}564                    <span className="muted">· {fmtDate(p.retraction.retractedAt)}</span>565                    <p className="mt-1">{p.retraction.notice}</p>566                  </li>567                )}568              </ul>569            )}570          </div>571        </section>572573        {p.related.length > 0 && (574          <section aria-labelledby="related" className="section-tight border-t hairline">575            <SectionHeading eyebrow="Related" title="Related working papers" id="related">576              Selected by shared discipline or keywords recorded in verified metadata.577            </SectionHeading>578            <ul className="mt-6 grid gap-4 md:grid-cols-2">579              {p.related.map((r) => (580                <li key={r.publicId}>581                  <PaperCard p={r} />582                </li>583              ))}584            </ul>585          </section>586        )}587        <div className="pb-6">588          <Disclosure />589        </div>590      </div>591592      {/* mobile sticky action bar */}593      {!withdrawn && (594        <div className="mobile-bar flex items-center gap-2 lg:hidden">595          <span className="mono-id min-w-0 flex-1 truncate">596            {p.paper.publicId} · v{v.versionNo}597          </span>598          <a href="#pdf" className="btn btn-sm">599            Read600          </a>601          <a href={`${v.pdfPublicPath}?download=1`} className="btn btn-primary btn-sm">602            <IconDownload size={14} /> PDF603          </a>604        </div>605      )}606    </article>607  );608}609
Files · main
Menu

Theme